{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fine tuning StarCoder2 with NeMo Framework"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introduction\n",
    "\n",
    "StarCoder2 is a newly improved open coding assistant model. The 15B model outperforms leading open code LLMs on popular programming benchmarks and delivers superior performance in its class. Notably, with a context length of 16,000 tokens, StarCoder2 model can handle a longer code base and elaborate coding instructions, get a better understanding of code structure, and provide improved code documentation. This model is the outcome of the collaboration among BigCode, ServiceNow, and NVIDIA. StarCoder2 is more powerful than StarCoder with doubled context window. With NVIDIA NeMo framework, you can customize StarCoder2 to fit your usecase and deploy an optimized model on your NVIDIA GPU.\n",
    "\n",
    "In this tutorial, we'll go over a popular Parameter-Efficient Fine-Tuning (PEFT) customization technique -- i.e. Low-Rank Adaptation (also known as LoRA) which enables the already upgraded StarCoder2 model to learn a new coding language or coding style.\n",
    "\n",
    "Note that the subject 15B StarCoder2 model takes 30GB disk space and requires more than 80GB CUDA memory while performing PEFT on a single GPU. Therefore, the verified hardware configuration for this notebook and the subsequent [inference notebook](https://github.com/NVIDIA/GenerativeAIExamples/blob/main/models/StarCoder2/inference.ipynb) employ a single node machine with 8 80GB NVIDIA GPUs."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Download the base model\n",
    "\n",
    "For all of our customization and deployment processes, we'll need to start off with a pre-trained model of StarCoder2. You can download the base model from Hugging Face (HF). Before doing that, make sure you have registered a HF account, consent to the StarCoder2 terms, and install the required libraries and Python packages to download the dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "!python -m pip install --upgrade pip\n",
    "!pip install --upgrade \"huggingface_hub[cli]\"\n",
    "!apt update\n",
    "!apt -y install git-lfs\n",
    "!git lfs install"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once done, clone the model from HF with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "\n",
    "!git clone git@hf.co:bigcode/starcoder2-15b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Getting NeMo Framework\n",
    "\n",
    "NVIDIA NeMo Framework is a generative AI framework built for researchers and PyTorch developers working on large language models (LLMs), multimodal models (MM), automatic speech recognition (ASR), and text-to-speech synthesis (TTS). The primary objective of NeMo is to provide a scalable framework for researchers and developers from industry and academia to more easily implement and design new generative AI models by being able to leverage existing code and pretrained models.\n",
    "\n",
    "If you haven't already, you can pull a container that includes the version of NeMo Framework and all dependencies needed for this notebook with the following:\n",
    "docker pull nvcr.io/nvidia/nemo:24.01.starcoder2\n",
    "\n",
    "The best way to run this notebook is from within the container. You can do that by launching the container with the following command:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "docker run --gpus device=all --shm-size=8g --net=host --ulimit memlock=-1 --rm -it -v ${PWD}:/workspace -w /workspace -v ${PWD}/results:/results nvcr.io/nvidia/nemo:24.01.starcoder2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "From within the container, start the Jupyter server with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "jupyter lab --no-browser --port=8080 --allow-root --ip 0.0.0.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will need to convert newly donwloaded HF model to .nemo format to perform PEFT.  First, let's upgrade the transformers package to the latest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "!pip install git+https://github.com/huggingface/transformers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, execute the following command to convert the StarCoder2 checkpoint into a .nemo file. Please make sure all the arguments point to the correct paths. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "%%bash\n",
    "python /opt/NeMo/scripts/nlp_language_modeling/convert_hf_starcoder2_to_nemo.py --in-file starcoder2-15b --out-file starcoder2-15b_base.nemo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Preparation\n",
    "\n",
    "Next, we'll need to prepare the data that we're going to use for our LoRA fine tuning. Here we're going to be using [Alpaca Python Code Instructions Dataset](https://huggingface.co/datasets/iamtarun/python_code_instructions_18k_alpaca), and training our model to enhance its instruction following ability for generating Python code."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's download Alpaca Python Code Instructions dataset from Hugging Face:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!git clone git@hf.co:datasets/iamtarun/python_code_instructions_18k_alpaca"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, the following code snippets convert the dataset into the JSONL format that NeMo defaults for PEFT. Meanwhile, we will reformat the data into list of (prompt, completion) pairs that our model can appropriately handle. Please refer to the printout for the original code instruction data format."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import glob\n",
    "from random import seed, shuffle\n",
    "from huggingface_hub import login\n",
    "\n",
    "login(token='your_huggingface_access_token')\n",
    "parquet_file_path = glob.glob('./python_code_instructions_18k_alpaca/data/*.parquet')\n",
    "parquet_file_list = ''.join(parquet_file_path)\n",
    "df = pd.read_parquet(parquet_file_list)\n",
    "instruct2code_list = df.to_dict('records')\n",
    "\n",
    "seed(2)\n",
    "val_percent = 5\n",
    "test_percent = 5\n",
    "instruct2code_list = instruct2code_list[:len(instruct2code_list)] \n",
    "num_train = int(len(instruct2code_list) * (100 - val_percent - test_percent) / 100)\n",
    "num_val = int(len(instruct2code_list)*(val_percent)/100)\n",
    "shuffle(instruct2code_list)\n",
    "\n",
    "instruct2code_list_train = instruct2code_list[:num_train]\n",
    "instruct2code_list_val = instruct2code_list[num_train : num_train + num_val]\n",
    "instruct2code_list_test = instruct2code_list[num_train + num_val:]\n",
    "print(f\"=== Input prompt example from the training split:\\n{instruct2code_list_train[5]['prompt']}\\n\") \n",
    "print(f\"=== Output completion example from the validation split:\\n{instruct2code_list_val[5]['output']}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "def write_jsonl(fname, json_objs):\n",
    "    with open(fname, 'wt') as f:\n",
    "        for o in json_objs:\n",
    "            f.write(json.dumps(o)+\"\\n\")\n",
    "\n",
    "def form_instruction(pair):\n",
    "    outpout_loc = pair.find('### Output')\n",
    "    return(pair[:outpout_loc])\n",
    "\n",
    "def convert_to_jsonl(instruct2code_list, output_path):\n",
    "    json_objs = []\n",
    "    for pair in instruct2code_list:\n",
    "        prompt = form_instruction(pair['prompt'])\n",
    "        completion = pair['output']\n",
    "        json_objs.append({\"input\": prompt, \"output\": completion})\n",
    "    write_jsonl(output_path, json_objs)\n",
    "    return json_objs\n",
    "\n",
    "train_json_objs = convert_to_jsonl(instruct2code_list_train, \"alpaca_python_train.jsonl\")\n",
    "val_json_objs = convert_to_jsonl(instruct2code_list_val, \"alpaca_python_val.jsonl\")\n",
    "test_json_objs = convert_to_jsonl(instruct2code_list_test, \"alpaca_python_test.jsonl\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's an example of what the data looks like after reformatting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_json_objs[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LoRA Configuration And PEFT\n",
    "### Step 1: Start NeMo Container\n",
    "\n",
    "If the container is not already running launch the following command:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "docker run --gpus device=all --shm-size=8g --net=host --ulimit memlock=-1 --rm -it -v ${PWD}:/workspace -w /workspace -v ${PWD}/results:/results nvcr.io/nvidia/nemo:24.01.starcoder2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Step 2: Run PEFT\n",
    "\n",
    "The megatron_gpt_peft_tuning_config.yaml file is referred to configure the parameters for the running PEFT training jobs in NeMo with LoRA technique for language model tuning. Let's point restore_from_path to the just converted .nemo file and dataset paths to the train and validation JSONL files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "\n",
    "torchrun --nproc_per_node=8 \\\n",
    "/opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_peft_tuning.py \\\n",
    "    trainer.devices=8 \\\n",
    "    trainer.num_nodes=1 \\\n",
    "    trainer.precision=bf16 \\\n",
    "    trainer.val_check_interval=20 \\\n",
    "    trainer.max_steps=50 \\\n",
    "    model.megatron_amp_O2=False \\\n",
    "    ++model.mcore_gpt=True \\\n",
    "    model.tensor_model_parallel_size=4 \\\n",
    "    model.pipeline_model_parallel_size=1 \\\n",
    "    model.micro_batch_size=1 \\\n",
    "    model.global_batch_size=8 \\\n",
    "    model.restore_from_path=path_to_nemo_file \\\n",
    "    model.data.train_ds.num_workers=0 \\\n",
    "    model.data.validation_ds.num_workers=0 \\\n",
    "    model.data.train_ds.file_names=[\"alpaca_python_train.jsonl\"] \\\n",
    "    model.data.train_ds.concat_sampling_probabilities=[1.0] \\\n",
    "    model.data.validation_ds.file_names=[\"alpaca_python_val.jsonl\"] \\\n",
    "    model.peft.peft_scheme=\"lora\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note: For running PEFT on multiple nodes (for example on a Slurm cluster, replace the torchrun --nproc_per_node=8 with python."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Step 3: Merge The Adapted Weights\n",
    "\n",
    "Once PEFT is finished, we'll need to merge the weights of the base model and the weights of the adapter. If you're using the NeMo Framework container, you'll find a script for this at /opt/NeMo/scripts/nlp_language_modeling/merge_lora_weights/merge.py. Otherwise, you can download the standalone script from GitHub at https://raw.githubusercontent.com/NVIDIA/NeMo/main/scripts/nlp_language_modeling/merge_lora_weights/merge.py. To run the merge script, you'll need the paths to the pretained .nemo model, the trained adapter .nemo model, as well as the path to save the merged model. Please modify according to your local environment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "\n",
    "python /opt/NeMo/scripts/nlp_language_modeling/merge_lora_weights/merge.py \\\n",
    "    trainer.accelerator=gpu \\\n",
    "    tensor_model_parallel_size=4 \\\n",
    "    pipeline_model_parallel_size=1 \\\n",
    "    gpt_model_file=starcoder2_base.nemo \\\n",
    "    lora_model_path=nemo_experiments/megatron_gpt_peft_lora_tuningch/megatron_gpt_peft_lora_tuning.nemo \\\n",
    "    merged_model_path=starcoder2_peft_merged.nemo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Step 4: Run Evaluation\n",
    "\n",
    "Run evaluation using [megatron_gpt_peft_eval.py](https://github.com/NVIDIA/NeMo/blob/main/examples/nlp/language_modeling/tuning/megatron_gpt_peft_eval.py)\n",
    "\n",
    "Set the appropriate model checkpoint path, test file path, batch sizes, number of tokens etc. and run evaluation on the test file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "%%bash\n",
    "\n",
    "python /opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_peft_eval.py \\\n",
    "    model.restore_from_path=starcoder2_peft_merged.nemo \\\n",
    "    trainer.devices=8 \\\n",
    "    model.global_batch_size=8 \\\n",
    "    model.data.test_ds.file_names=[\"alpaca_python_test.jsonl\"] \\\n",
    "    model.data.test_ds.names=[\"alpaca_python_test_set\"] \\\n",
    "    model.data.test_ds.global_batch_size=8 \\\n",
    "    model.data.test_ds.micro_batch_size=1 \\\n",
    "    model.data.test_ds.tokens_to_generate=20 \\\n",
    "    model.tensor_model_parallel_size=4 \\\n",
    "    model.pipeline_model_parallel_size=1 \\\n",
    "    inference.greedy=True \\\n",
    "    model.data.test_ds.output_file_path_prefix=/results/lora_results \\\n",
    "    model.data.test_ds.write_predictions_to_file=True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Check the output from the result file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tail -n 4 lora_results.jsonl"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note, This is only a sample output (based of a toy LoRA example) and your output may vary. The performance can be further improved by fine tuning the model for more steps."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
